/*
 * Copyright 2005 MBARI
 *
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1
 * (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.gnu.org/copyleft/lesser.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package org.mbari.vcr.rs422;

import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.mbari.comm.BadPortException;
import org.mbari.comm.CommUtil;
import org.mbari.movie.Timecode;
import org.mbari.util.NumberUtil;
import org.mbari.vcr.IVCR;
import org.mbari.vcr.IVCRReply;
import org.mbari.vcr.VCRAdapter;
import org.mbari.vcr.VCRException;
import org.mbari.vcr.VCRUtil;

/**
 * <p>Control software for Sony VCR's. This class connects to the VCR using the
 * RS-422 (Sony 9-pin) protocol through a serial port (RS-232). Note that the
 * pin setting of RS-232 and RS-422 are <u>NOT</u> the same. Connecting to the
 * VCR will require a pin converter.</p>
 *
 * <p>When a command is sent to the VCR this class will get the VCR's reply and
 * update the various status objects(VCRReply, VCRTimecode, VCRError, and VCRState)
 * as needed. Access to the status objects can be obtained by using an IVCRManager
 * object. Get the IVCRManager object by calling VCR.getVcrManager();</p>
 *
 * <p><font color="ff3333">Requires the java COMM package.</font></p></p>
 *
 * @author : $Author: hohonuuli $
 * @version : $Revision: 332 $
 */
public class VCR extends VCRAdapter {

    /** Request the type of VCR (i.e. Device) */
    public final static byte[] DEVICE_TYPE_REQUEST = { 0x00, 0x11 };

    /** Eject command */
    public final static byte[] EJECT_TAPE = { 0x20, 0x0F };

    /** Fast-forward command */
    public final static byte[] FAST_FWD = { 0x20, 0x10 };

    /** get cue up status command */
    public final static byte[] GET_CUEUP_STATUS = { 0x61, 0x20, 0x21 };

    /** Get longitudinal timecode command */
    public final static byte[] GET_LTIMECODE = { 0x61, 0x0c, 0x01 };

    /** Get Longitudinal Userbits timecode command */
    public final static byte[] GET_LUBTIMECODE = { 0x61, 0x0c, 0x0F };

    /** Get status command */
    public final static byte[] GET_STATUS = { 0x61, 0x20, 0x03 };

    /**
     * This is a special command supplied by sony that returns the best source
     * of the timecode (VITC at slwo speeds, LTC at high speeds)
     */
    public final static byte[] GET_TIMECODE = { 0x61, 0x0c, 0x03 };

    /** Get Timer-1 timecode command */
    public final static byte[] GET_TIMECODE1 = { 0x61, 0x0c, 0x04 };

    /** Get Timer-2 timecode command */
    public final static byte[] GET_TIMECODE2 = { 0x61, 0x0c, 0x08 };

    /** Get vertical timecode command */
    public final static byte[] GET_VTIMECODE = { 0x61, 0x0c, 0x02 };

    /** Get vertical Userbits timecode command */
    public final static byte[] GET_VUBTIMECODE = { 0x61, 0x0c, 0x10 };

    /**
     * For RXTX we have to put the thread to sleep VERY briefly
     * in order for the serial io to work
     */
    private static final long IO_DELAY = 10;

    /** <!-- Field description --> */
    public final static byte[] LOCAL_DISABLE = { 0x00, 0x0c };

    /** <!-- Field description --> */
    public final static byte[] LOCAL_ENABLE = { 0x00, 0x1d };

    /**
     * Pause - this was passed to me from Danelle Cline for the JVC VCR. Ntt
     * sure if it works with Sony's. Not yet sure how to resume.
     */
    public final static byte[] PAUSE = { 0x21, 0x13, 0x00 };

    /** Play command */
    public final static byte[] PLAY_FWD = { 0x20, 0x01 };

    /** preset time code command */
    public final static byte[] PRESET_TIMECODE = {
        0x44, 0x04, 0x00, 0x00, 0x00, 0x00
    };

    /** get cue up status command */
    public final static byte[] PRESET_USERBITS = {
        0x44, 0x05, 0x00, 0x00, 0x00, 0x00
    };

    /**
     * Maximum receive timeout in millisecs
     */
    private final static long RECEIVE_TIMEOUT = 40;

    /** Record command */
    public final static byte[] RECORD = { 0x20, 0x02 };

    /** Release tape command */
    public final static byte[] RELEASE_TAPE = { 0x20, 0x04 };

    /** Rewind command */
    public final static byte[] REWIND = { 0x20, 0x20 };

    /**
     * Shuttle forward command. Last byte should be modified to provide the
     * shuttle speed (byte value between 0 (stopped) and 255 (very fast))
     */
    public final static byte[] SHUTTLE_FWD = { 0x21, 0x13, 0x00 };

    /**
     * Shuttle reverse command. Last byte should be modified to provide the
     * shuttle speed (byte value between 0 (stopped) and 255 (very fast))
     */
    public final static byte[] SHUTTLE_REV = { 0x21, 0x23, 0x00 };

    /**
     * Seek time code command. Last four bytes need to be modified to
     * specify the timecode to seek to. Refer to the VCR documentation for the format of the timecode bytes.
     */
    public final static byte[] SONY_SEEK_TIMECODE = {
        0x24, 0x31, 0x00, 0x00, 0x00, 0x00
    };

    /** Stop command */
    public final static byte[] STOP = { 0x20, 0x00 };
    private static final Logger log = LoggerFactory.getLogger(VCR.class);

    /**
     *     @uml.property  name="in"
     */
    private InputStream in;    // Reads responses from VCR

    /**
     *     @uml.property  name="maxRetrys"
     */
    private int maxRetrys = 5;    // No of attempts to try connecting

    /**
     *     For storage of the mostRecently submitted command to the VCR. When a reply is returned by the vcr this command is updated in the vcrReplyBuffer
     *     @uml.property  name="mostRecentCommand" multiplicity="(0 -1)" dimension="1"
     */
    private byte[] mostRecentCommand;

    /**
     *     Need to hang on to the check sum. the VCRReply object needs this.
     *     @uml.property  name="out"
     */

    //private byte[] mostRecentCommandChecksum;
    private OutputStream out;    // Sends commands to VCR

    /**
     *     @uml.property  name="portId"
     *     @uml.associationEnd  multiplicity="(1 1)"
     */
    private CommPortIdentifier portId;    // Java port Identifier

    /**
     *     @uml.property  name="portName"
     */
    private String portName;    // Serial port name (e.g. COM1)

    //private byte[] resumeCommand;         // Not implemented yet. To be used for pause.

    /**
     *     @uml.property  name="retryConnection"
     */
    private boolean retryConnection = true;

    /**
     *     @uml.property  name="retryCommand"
     */
    private boolean retryCommand = false;

    /**
     *     @uml.property  name="serialPort"
     *     @uml.associationEnd  multiplicity="(1 1)"
     */
    private SerialPort serialPort;    // Serial port connected to VCR


    /**
     * @param portName
     * @throws BadPortException
     * @throws IOException
     */
    public VCR(String portName) throws BadPortException, IOException {
        vcrReply = new VCRReply();
        this.portName = portName;
        initComm();    // Set up comm
        requestLTimeCode();
        requestStatus();
    }

    /**
     * Query the VCR for its type. Return codes are MODEL        Data-1      Data-2<br> BVH-2000(00) 00  11<br>
     * BVH-2000(02) 00  10<br>
     */
    public void deviceTypeRequest() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Device Type Request [" + NumberUtil.toHexString(DEVICE_TYPE_REQUEST) +
                      "]");
        }

        sendCommand(DEVICE_TYPE_REQUEST);
    }

    /** Disconnect serial port communications */
    public void disconnect() {
        try {
            removeAllObservers();
            out.close();
            in.close();
            serialPort.close();
            getVcrTimecode().getTimecode().setTime(0, 0, 0, 0);
            ((VCRState) getVcrReply().getVcrState()).setStatus(0);
        }
        catch (Exception e) {
            if (log.isErrorEnabled() && (serialPort != null)) {
                log.error("Problem occured when closing serial port communications on " + serialPort.getName());
            }
        }
    }

    /** Eject a tape, updates status. */
    public void eject() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Eject [" + NumberUtil.toHexString(EJECT_TAPE) + "]");
        }

        sendCommand(EJECT_TAPE);
        requestStatus();
    }

    /** Fast forward the tape, updates status. */
    public void fastForward() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Fast Forward [" + NumberUtil.toHexString(FAST_FWD) + "]");
        }

        sendCommand(FAST_FWD);
        requestStatus();
    }

    /**
     * sets up communications with a Sony VTR (i.e VCR)
     *
     * @throws BadPortException
     */
    private void initComm() throws BadPortException {

        // Test the port to see if its working. Retry on certain errors.
        int numAttempts = 0;    // Count the number of connection atempts
        while ((portName != null) && (retryConnection)) {
            numAttempts++;

            /*
             *  Check port status. Certain errors can be overcome by attempting
             *  to connect again.
             */
            if (log.isDebugEnabled()) {
                log.debug("Initializing serial port communication on " + portName);
            }

            portId = getPortIdentifier();

            if (numAttempts > maxRetrys) {
                retryConnection = false;

                break;    // Don't bother to pause if the connection fails.
            }

            /*
             *  Pause for 1 second after each attempt to communicate. This gives
             *  the ports a chance for resources to get closed, established, etc.
             */
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
                log.error("The VCR connection was interrupted while waiting for " +
                          "communications to be established. The state of the connection " +
                          " is unknown. If you experience problems restart the appliation.");
                Thread.currentThread().interrupt();
            }

            // No portID?..then exit out of this program by tossing an exception
            if (portId == null) {
                throw new BadPortException("Serial Port Communications failed");
            }

            // Open the serial port for reading and writing
            try {
                serialPort = (SerialPort) portId.open("VCR", 2000);
                serialPort.setSerialPortParams(38400, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
                                               SerialPort.PARITY_ODD);
                serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN);
                ((CommPort) serialPort).enableReceiveTimeout((int) RECEIVE_TIMEOUT);    // Returns after waiting 190ms
                in = serialPort.getInputStream();
                out = serialPort.getOutputStream();
            }
            catch (Exception e) {
                throw new BadPortException(e.getMessage());
            }
        }
    }

    /**
     * Pause the tape. This is not yet implemented
     *
     */
    public void pause() {

        // Store the last command
        //resumeCommand = vcrReply.getCommand();

        // TODO 20040203 brian: Insert pause command here
        requestStatus();
    }

    /** Play the tape, updates status. */
    public void play() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Play [" + NumberUtil.toHexString(PLAY_FWD) + "]");
        }

        sendCommand(PLAY_FWD);
        requestStatus();
    }

    /**
     * Set the timecode on the VCR.
     * @param timecode a byte array representing the time code value
     */
    public void presetTimecode(byte[] timecode) {
        byte[] command = PRESET_TIMECODE;
        for (int i = 0; i < timecode.length; i++) {
            command[2 + i] = timecode[i];
        }

        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Preset Timecode [" + NumberUtil.toHexString(command) + "]");
        }

        sendCommand(command);
        requestStatus();
    }

    /**
     * Set the Userbits on the VCR
     *
     * @param userbits
     */
    public void presetUserbits(byte[] userbits) {
        byte[] command = PRESET_USERBITS;
        for (int i = 0; i < userbits.length; i++) {
            command[2 + i] = userbits[i];
        }

        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Preset Userbits [" + NumberUtil.toHexString(command) + "]");
        }

        sendCommand(command);
        requestStatus();
    }

    /** Start recording, updates status. */
    public void record() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Record [" + NumberUtil.toHexString(RECORD) + "]");
        }

        sendCommand(RECORD);
        requestStatus();
    }

    /** Release the tape, updates status. */
    public void releaseTape() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Release Tape [" + NumberUtil.toHexString(RELEASE_TAPE) + "]");
        }

        sendCommand(RELEASE_TAPE);
        requestStatus();
    }

    /**
     * Get longitudial timecode, which is stored on the audio track. Not as
     * accurate as getVTimeCode, but this method will get timecodes during fast-forwards, rewinds and shuttiling
     */
    public void requestLTimeCode() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Longitudinal Timecode [" +
                      NumberUtil.toHexString(GET_LTIMECODE) + "]");
        }

        sendCommand(GET_LTIMECODE);
    }

    /**
     * <p><!-- Method description --></p>
     *
     */
    public void requestLUserbits() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Longitudinal Userbits [" +
                      NumberUtil.toHexString(GET_LUBTIMECODE) + "]");
        }

        sendCommand(GET_LUBTIMECODE);
    }

    /**
     * <p><!-- Method description --></p>
     *
     */
    public void requestLocalDisable() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Local Disable [" + NumberUtil.toHexString(LOCAL_DISABLE) + "]");
        }

        sendCommand(LOCAL_DISABLE);
    }

    /**
     * <p><!-- Method description --></p>
     *
     */
    public void requestLocalEnable() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Local Enable [" + NumberUtil.toHexString(LOCAL_ENABLE) + "]");
        }

        sendCommand(LOCAL_ENABLE);
    }

    /** Send a "get status" command to the VCR */
    public void requestStatus() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Status [" + NumberUtil.toHexString(GET_STATUS) + "]");
        }

        sendCommand(GET_STATUS);
    }

    /**
     * When requesting a timecode, the results depend on the tape speed because
     * at very low speeds (> 0.25 play speed) it may not be possible to recover
     * the timecode. However, if VITC is present it may be used instead. To
     * automate the decision process, Sony has provided a special command which
     * will return the best source of timecode. Note that when LTC and VITC are
     * both garbage you get back the 74.14 corrected LTC data. In this case, the
     * time is actually the last good LTC time corrected by the tape timer.
     *
     */
    public void requestTimeCode() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Timecode [" + NumberUtil.toHexString(GET_TIMECODE) + "]");
        }

        sendCommand(GET_TIMECODE);
    }

    /**
     * Tries to request the best userbits. This method has not been tested.
     *
     */
    public void requestUserbits() {
        if (getVcrState().isPlaying()) {
            requestVUserbits();
        }
        else {
            requestLUserbits();
        }
    }

    /**
     * Get vertical timecode, which is stored between video frames. These
     * timecodes can not be accessed in any mode accept play mode. So use
     * getLTimeCode when fast-forwarding, rewinding, or shuttling
     */
    public void requestVTimeCode() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Vertical Timecode [" + NumberUtil.toHexString(GET_VTIMECODE) +
                      "]");
        }

        sendCommand(GET_VTIMECODE);
    }

    /**
     * <p><!-- Method description --></p>
     *
     */
    public void requestVUserbits() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Request Vertical Userbits [" + NumberUtil.toHexString(GET_VUBTIMECODE) +
                      "]");
        }

        sendCommand(GET_VUBTIMECODE);
    }

    /** Resumes the last command that was paused. This is not yet implemented */
    public void resume() {

//      No implementation
    }

    /** Rewind the tape, updates status. */
    public void rewind() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Rewind [" + NumberUtil.toHexString(REWIND) + "]");
        }

        sendCommand(REWIND);
        requestStatus();
    }

    /**
     * Fast forward to the specified timecode, updates status.
     * @param timecode a byte array representing the time code value
     * @see VCRUtil
     */
    public void seekTimecode(byte[] timecode) {
        byte[] command = SONY_SEEK_TIMECODE;
        for (int i = 0; i < timecode.length; i++) {
            command[2 + i] = timecode[i];
        }

        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Seek Timecode [" + NumberUtil.toHexString(command) + "]");
        }

        sendCommand(command);
        requestStatus();
    }

    /**
     * Fast forward to the specified timecode, updates status.
     * @param timecode
     */
    public void seekTimecode(int timecode) {
        byte[] command = SONY_SEEK_TIMECODE;
        byte[] byteTimecode = NumberUtil.toByteArray(timecode);
        for (int i = 0; i < byteTimecode.length; i++) {
            command[2 + i] = byteTimecode[i];
        }

        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Seek Timecode [" + NumberUtil.toHexString(command) + "]");
        }

        sendCommand(command);
        requestStatus();
    }
    
    

    /**
     * Sends a command, in the format of a byte[], to the VCR. This class will
     * re-attempt to send the command up to 5 times if a recoverable error occurs.
     * @param command The command to send to the VCR
     */
    protected synchronized void sendCommand(byte[] command) {
        if (portName == null) {
            return;
        }

        // Store the last command
        this.mostRecentCommand = command;
        byte checksum = VCRReply.calculateChecksum(command);

        int numAttempts = 0;    // Counter of attempts to send current command
        do {                             // Retry sending the command if an error occurs
            try {

                final byte[] c = new byte[command.length + 1];
                System.arraycopy(command, 0, c, 0, command.length);
                c[c.length - 1] = checksum;

                if (log.isDebugEnabled()) {
                    log.debug("Sending bytes '" + NumberUtil.toHexString(c) + "' to " + portName);
                }

                numAttempts++;           // Increment the number of attempts to send command
                retryCommand = false;    // Don't retry. Unless an error occurs

                out.write(c);

                try {
                    Thread.sleep(IO_DELAY);
                    getResponse();
                }
                catch (InterruptedException e) {
                    log.error("Thread " + Thread.currentThread().getName() + " was interrupted", e);
                    Thread.currentThread().interrupt();
                }


                if (vcrReply.isNack()) {
                    retryCommand = true;
                }
            }
            catch (IOException e) {
                retryCommand = true;
                log.error("Failed to send a command to the VCR", e);
            }
            catch (VCRException e) {
                retryCommand = true;
                log.error("Failed to send a command to the VCR", e);
            }
        }
        while ((numAttempts <= maxRetrys) && (retryCommand));
    }

    /**
     * shuttle foward using the giving speed, updates status.
     * @param speed value between 0 (slow) and 255 (fast)
     */
    public void shuttleForward(int speed) {

        // Last byte in this array is the speed
        byte[] command = SHUTTLE_FWD;

        // put the correct speed in the byte array
        byte[] byteSpeed = NumberUtil.toByteArray(speed);
        command[2] = byteSpeed[3];    // speed is between 0-255, ignore other bytes

        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Shuttle Forward [" + NumberUtil.toHexString(command) + "]");
        }

        sendCommand(command);
        requestStatus();
    }

    /**
     * shuttle backwards using the giving speed, updates status.
     * @param speed value between 0 (slow) and 255 (fast)
     */
    public void shuttleReverse(int speed) {

        // Last byte in this array is the speed
        byte[] command = SHUTTLE_REV;

        // put the correct speed in the byte array
        byte[] byteSpeed = NumberUtil.toByteArray(speed);
        command[2] = byteSpeed[3];    // speed is between 0-255, ignore other bytes

        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Shuttle Reverse [" + NumberUtil.toHexString(command) + "]");
        }

        sendCommand(command);
        requestStatus();
    }

    /** Stop the tape, updates status. */
    public void stop() {
        if (log.isDebugEnabled()) {
            log.debug("Sending VCR Command -> Stop [" + NumberUtil.toHexString(STOP) + "]");
        }

        sendCommand(STOP);
        requestStatus();
    }

    /**
     * Returns a name for this connection
     *
     * @return
     */
    public String getConnectionName() {
        return (portName == null) ? "Not Connected" : portName;
    }

    /**
     *     @return  The serial port that the VCR is using to communicate with the VCR
     *     @uml.property  name="portId"
     */
    public CommPortIdentifier getPortId() {
        return portId;
    }

    /**
     * Method for checking the status of a named commport for all the operations
     * needed for contacting the VCR through a serial port
     * @return ID for the VCR port specified. null if the operation failed
     */
    private CommPortIdentifier getPortIdentifier() {
        CommPortIdentifier thisCpi = null;
        Enumeration ports = CommPortIdentifier.getPortIdentifiers();
        while (ports.hasMoreElements()) {
            CommPortIdentifier cpi = (CommPortIdentifier) ports.nextElement();
            if (cpi.getName().equalsIgnoreCase(portName)) {
                SerialPort portTmp = null;
                try {
                    portTmp = (SerialPort) cpi.open("Checking for " + "comm port identifier", 30);
                    portTmp.setSerialPortParams(38400, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
                                                SerialPort.PARITY_ODD);
                    portTmp.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN & SerialPort.FLOWCONTROL_RTSCTS_OUT);
                    OutputStream outStream = portTmp.getOutputStream();
                    InputStream inStream = portTmp.getInputStream();
                    outStream.close();
                    inStream.close();
                    portTmp.close();
                    thisCpi = cpi;
                    this.retryConnection = false;

                    break;    // Exit out of the while loop when the correct commport is found
                }
                catch (PortInUseException e) {

                    // Port in use by another program
                    retryConnection = false;

                    //vcrError.setError(VCRError.ES_NOCONNECT);
                }
                catch (IOException e) {

                    // Problem with port, doesn't support I/O
                    retryConnection = true;
                    portTmp.close();
                }
                catch (UnsupportedCommOperationException e) {

                    // Unable to set port to 38400 baud, 8O1, RTSCTS IN/OUT
                    retryConnection = true;
                    portTmp.close();
                }
            }
        }

        return thisCpi;
    }

    /**
     * Reads the response to a command from the serial port connected to the VCR.
     * @throws IOException
     * @throws VCRException
     * @throws InterruptedException
     */
    private synchronized void getResponse() throws IOException, VCRException, InterruptedException {

        // Get the command returned by the VCR
        final byte[] cmd = new byte[2];

        if (in.available() > 0) {
            in.read(cmd);
        }

        Thread.sleep(IO_DELAY);

        // Extract the number of data bytes in the command block. Then
        // read the data from the serial port
        final int numDataBytes = (int) (cmd[0] & 0x0F);    // Get the number of data blocks
        byte[] data = null;
        if (numDataBytes > 0) {
            data = new byte[numDataBytes];

            if (in.available() > 0) {
                in.read(data);
            }
            else {
                throw new IOException("Incoming data is missing . byte[] = " + NumberUtil.toHexString(cmd));
            }
        }

        Thread.sleep(IO_DELAY);

        // Read the checksum that the VCR sends
        final byte[] checksum = new byte[1];
        if (in.available() > 0) {
            in.read(checksum);
        }
        else {
            throw new IOException("Incoming checksum is missing. cmd[] =  " + NumberUtil.toHexString(cmd) +
                                  " data[] = " + NumberUtil.toHexString(data));
        }

        ((org.mbari.vcr.rs422.VCRReply) vcrReply).update(mostRecentCommand, cmd, data, checksum);

        if (log.isDebugEnabled()) {

            /*
             * Munge it all into a single byte array
             */
            int dataLength = (data == null) ? 0 : data.length;
            final byte[] c = new byte[cmd.length + dataLength + 1];
            System.arraycopy(cmd, 0, c, 0, cmd.length);

            if (data != null) {
                System.arraycopy(data, 0, c, cmd.length, data.length);
            }

            c[c.length - 1] = checksum[0];

            log.debug("<< " + NumberUtil.toHexString(c));
        }
    }

    /**
     *  Method used to iterate through the communication ports looking for the serial port with the VCR attached to it.
     * @return The port with the Sony VCR hooked up to it. If no VCR is found <b>null<b/> is returned
     */
    public static CommPortIdentifier getVcrPort() {

        // Get a set of ports that are not in use
        final HashSet serialPorts = CommUtil.getAvailableSerialPorts();
        final Iterator i = serialPorts.iterator();
        CommPortIdentifier port = null;
        CommPortIdentifier vcrPort = null;
        while (i.hasNext()) {
            try {
                port = (CommPortIdentifier) i.next();
                final IVCR vcr = new VCR(port.getName());
                vcr.requestStatus();
                final IVCRReply reply = vcr.getVcrReply();
                final boolean isStatusReply = (reply.isStatusReply() || reply.isAck() || reply.isNack() ||
                                               reply.isTimecodeReply() || reply.isUserbitsReply());
                vcr.disconnect();

                if (isStatusReply) {
                    vcrPort = port;

                    break;
                }
            }
            catch (BadPortException e) {
                if (log.isWarnEnabled()) {
                    log.warn("Problem with " + port, e);
                }
            }
            catch (IOException e) {
                if (log.isWarnEnabled()) {
                    log.warn("Problem with " + port, e);
                }
            }
        }

        return vcrPort;
    }

    /**
     * Method description
     *
     *
     * @return
     */
    public IVCRReply getVcrReply() {
        return vcrReply;
    }

    public void seekTimecode(Timecode timecode) {
        byte[] time = new byte[] { VCRUtil.intToTime(timecode.getFrame()),
                            VCRUtil.intToTime(timecode.getSecond()),
                            VCRUtil.intToTime(timecode.getMinute()),
                            VCRUtil.intToTime(timecode.getHour()) };
        seekTimecode(time);
    }
}
